##############
# (*) Kaleidoscope
# By: Allan Spale
# August 2008

from __future__ import with_statement

import sys
import uuid

from Kaleidoscope.KSColumn import KSColumnError
from Kaleidoscope.KSTransaction import KSTransaction
from Kaleidoscope.KSUtility import KSUtility
from Kaleidoscope.KSColumnMapped import KSColumnMapped, KSColumnMappedError
from Kaleidoscope.KSEventManager import KSEventManager
from Kaleidoscope.KSCursorError import KSCursorError
from Kaleidoscope.KSColumn import KSColumn


class KSColumnMappedConstraintError(Exception):
    """
    Exception class for KSColumnMapped

    """
    # REDO ALL CONSTANT PROPERTIES TO BE NORMAL/WRITABLE OUTSIDE OF CLASS...
    # FIX THIS LATER

    B_UNKNOWN_EXCEPTION = True
    B_CLASS_EXCEPTION = False

    B_PRIMARY_COLUMN = True
    B_SECONDARY_COLUMN = False

    def __init__(self, u_message):
        self.__u_message = u_message
    def __str__(self):
        return repr(self.__u_message)

    @staticmethod
    def print_and_raise_error(u_message, b_unknown_exception):
        """
        Convenience method for printing and raising an error for this
        particular Exception subclass.

        """
        #sys.stderr.write(StringIO.StringIO(sys.exc_info()))
            # write initial error
        u_message = u'KSColumnMappedConstraint.py: ' + u_message
        sys.stderr.write(u_message + u'\n')

        if (b_unknown_exception):
            raise Exception(u_message)
        else:
            raise KSColumnMappedConstraintError(u_message)


class KSColumnMappedConstraint(KSColumnMapped):
    """
    This class is mostly like KSColumnMapped in structure except that instead
    of implicitly storing and retrieving values from the secondary
    column, simply make sure that the secondary key itself is returned rather
    than the data. Also, when primitive types are used as the type, make sure
    that type checking occurs before a change or insert to validate the type.

    """
    def __init__(self, ksc_key_column, ksc_secondary_column):
        """
        Create the mapped column with the primary column (ksc_key_column) and
        the secondary column (ksc_secondary_column). Unlike the superclass,
        this class will not map values; it will simply check to make sure that
        values assigned to the secondary column actually exist in its mapped
        column.

        """
        try:
            super(KSColumnMappedConstraint, self).__init__(
                ksc_key_column, ksc_secondary_column)

            # Handle the potential mix-in case
            #if (isinstance(ksc_secondary_column, unicode)):
            #    KSColumnMappedConstraint.__bases__ += \
            #        (KSMappedConstraintMixInVirtualType,)

            self.__ksc_key_column = ksc_key_column
            self.__ksc_secondary_column = ksc_secondary_column
            self.__id = uuid.uuid4()
            self.__ksem_event_manager = KSEventManager(self.__id)
            self.__ksem_event_manager.register_all_functions_from_class(
                KSColumnMappedConstraint, 950)
            self.__context = KSUtility(
                self.__ksem_event_manager, self.get_transaction)
            #self.__ksc_secondary_column_type = \
            #    self.__ksc_secondary_column.u_key_type

        except Exception:
            KSColumnMappedConstraintError.print_and_raise_error(
                u'An unknown error prevented KSColumnMappedConstraint from '
                + u'being initialized.', KSColumnError.B_UNKNOWN_EXCEPTION)

    # use open, close from parent

    def get_data_from_key(self, obj_key, kstx_parent_transaction,
                              b_packed_data):
        """
        Retrieve the actual data stored with relation to the value in obj_key.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.insert_item_with_key_value_pair, [object, KSTransaction,
                bool], [obj_key, kstx_parent_transaction, b_packed_data]) \
                as kstx_transaction:

                result = self.__ksc_key_column.get_data_from_key(
                    obj_key, kstx_transaction, b_packed_data)

            self.__ksem_event_manager.send_after_event_message(
                self.get_data_from_key, [result])
            return result

        except Exception:
            raise


    def get_keys_from_data_value(self, obj_target_data,
                                     kstx_parent_transaction, b_packed_data):
        """
        From the data value specified in obj_target_data, retrieve the values
        assigned to it.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.get_keys_from_data_value, [object, KSTransaction,
                bool], [obj_target_data, kstx_parent_transaction,
                b_packed_data]) as kstx_transaction:

                result = self.__ksc_key_column.get_keys_from_data_value(
                    obj_target_data, kstx_transaction, b_packed_data)

            self.__ksem_event_manager.send_after_event_message(
                self.get_keys_from_data_value, [result])
            return result

        except Exception:
            raise


    def get_keys_from_data_object_range(self, obj_start_data, obj_end_data,
            i_low_comparison_operator, i_high_comparison_operator,
            kstx_parent_transaction, b_return_packed_data):
        """
        From the range of data values specified by the values of the endpoints
        in obj_start_data and obj_end_data and further clarified with the
        comparison operators i_low_comparison_operator and
        i_high_comparison_operator, return the keys that are mapped to the
        values in this range of data values.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.get_keys_from_data_object_range, [object, object,
                int, int, KSTransaction, bool], [obj_start_data, obj_end_data,
                i_low_comparison_operator, i_high_comparison_operator,
                kstx_parent_transaction, b_return_packed_data]) \
                as kstx_transaction:

                result = self.__ksc_key_column.get_keys_from_data_object_range(
                    obj_start_data, obj_end_data, i_low_comparison_operator,
                    i_high_comparison_operator, kstx_transaction,
                    b_return_packed_data)

            self.__ksem_event_manager.send_after_event_message(
                self.get_keys_from_data_object_range, [result])
            return result

        except Exception:
            raise


    def get_item_from_partial_key(self, obj_partial_key,
                                      kstx_parent_transaction):
        """
        Based on a partial key (appropriate for unicode strings), return its
        related value.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.get_item_from_partial_key, [object, KSTransaction],
                [obj_partial_key, kstx_parent_transaction]) \
                as kstx_transaction:

                result = self.__ksc_key_column.get_item_from_partial_key(
                    obj_partial_key, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.get_item_from_partial_key, [result])
            return result

        except Exception:
            raise


    def insert_item_with_key_value_pair(self, obj_key, obj_data,
                                            kstx_parent_transaction):
        """
        Insert the key/data pair. The key will be stored in the primary
        column. The data value will be checked in the secondary database
        for a unique key. This secondary key will then be stored as the
        data item for the primary database.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.insert_item_with_key_value_pair, [object, object,
                KSTransaction], [obj_key, obj_data, kstx_parent_transaction]) \
                as kstx_transaction:

                b_result = self.__check_for_secondary_key(
                    obj_data, kstx_transaction)

                if (b_result):
                    result = self.__ksc_key_column.\
                        insert_item_with_key_value_pair(
                            obj_key, obj_data, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.insert_item_with_key_value_pair, [result])
            return result

        except Exception:
            raise


    def insert_value_and_generate_key(self, obj_data,
                                          kstx_parent_transaction):
        """
        Insert data with a generated key. The key will be stored in the primary
        column. The data value will be checked in the secondary database
        for a unique key. This secondary key will then be stored as the
        data item for the primary database.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.insert_value_and_generate_key, [object,
                KSTransaction], [obj_data, kstx_parent_transaction]) \
                as kstx_transaction:

                b_result = self.__check_for_secondary_key(
                    obj_data, kstx_transaction)

                if (b_result):
                    result = self.__ksc_key_column.\
                        insert_value_and_generate_key(
                            obj_data, kstx_parent_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.insert_value_and_generate_key, [result])
            return result

        except Exception:
            raise


    def insert_items_with_key_and_value_list(self, obj_key, list_values,
                                                 kstx_parent_transaction):
        """
        Insert the key/values pair. Need to watch for the case where there only
        was a NULL value assigned to this key. For this reason, the new
        value(s) should displace the NULL value.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.insert_items_with_key_and_value_list, [object, list,
                KSTransaction], [obj_key, list_values,
                kstx_parent_transaction]) as kstx_transaction:

                if (obj_key == KSColumn.NULL_VALUE):
                    KSColumnError.print_and_raise_error(u'The insertion of a '
                        + u'NULL key is prohibited.',
                        KSColumnError.B_CLASS_EXCEPTION)

                # Before doing any insertions, check to make sure that
                # each of the values in the list exists as a secondary
                # key. If even one does not exist, reject the entire list.
                b_result = True
                for obj_data in list_values:
                    b_result &= self.__check_for_secondary_key(
                        obj_data, kstx_transaction)

                if (b_result):
                    result = self.__ksc_key_column.\
                        insert_items_with_key_and_value_list(
                            obj_key, list_values, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.insert_items_with_key_and_value_list, [result])
            return result

        except Exception:
            raise


    def change_key(self, obj_old_key, obj_new_key, kstx_parent_transaction):
        """
        Get the data list from the old key, and store it as a new key
        value list.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.change_key, [object, object,
                KSTransaction], [obj_old_key, obj_new_key,
                kstx_parent_transaction]) as kstx_transaction:

                result = self.__ksc_key_column.change_key(
                    obj_old_key, obj_new_key, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.change_key, [result])
            return result

        except Exception:
            raise


    def change_all_data_values(self, obj_old_data_value, obj_new_data_value,
                                   kstx_parent_transaction):
        """
        Essentially a search and replace. Find the old data values given in
        obj_old_data_value in the secondary column and replace them with the
        new data values given in obj_new_data_value.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.change_all_data_values, [object, object,
                KSTransaction], [obj_old_data_value, obj_new_data_value,
                kstx_parent_transaction]) as kstx_transaction:

                b_result = self.__check_for_secondary_key(
                    obj_old_data_value, kstx_transaction) and \
                    self.__check_for_secondary_key(
                    obj_new_data_value, kstx_transaction)

                if (b_result):
                    result = self.__ksc_key_column.change_all_data_values(
                        obj_old_data_value, obj_new_data_value,
                        kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.change_all_data_values, [result])
            return result

        except Exception:
            raise


    def change_key_value_pair(self, obj_key, obj_data, kstx_parent_transaction):
        """
        Change the data for the given key.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.change_key_value_pair, [object, object,
                KSTransaction], [obj_key, obj_data, kstx_parent_transaction]) \
                as kstx_transaction:

                b_result = self.__check_for_secondary_key(
                    obj_data, kstx_transaction)

                if (b_result):
                    result = self.__ksc_key_column.\
                        change_key_value_pair(
                            obj_key, obj_data, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.change_key_value_pair, [result])
            return result

        except Exception:
            raise


    def delete_item_by_key(self, obj_key, kstx_parent_transaction):
        """
        Based on the value in the key obj_key, delete its relation from the
        table.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.delete_item_by_key, [object, KSTransaction],
                [obj_key, kstx_parent_transaction]) as kstx_transaction:

                result = self.__ksc_key_column.delete_item_by_key(
                    obj_key, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.delete_item_by_key, [result])
            return result

        except Exception:
            raise


    def delete_items_by_value_list(self, list_values,
                                       kstx_parent_transaction):
        """
        Given a list of values in list_values, delete their related keys which
        will in turn remove the relations from the table.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.delete_items_by_value_list, [list, KSTransaction],
                [list_values, kstx_parent_transaction]) as kstx_transaction:

                result = self.__ksc_key_column.delete_items_by_value_list(
                    list_values, kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.delete_items_by_value_list, [result])
            return result

        except Exception:
            raise


    def get_all(self, kstx_parent_transaction, b_return_packed_data):
        """
        Retrieve all values in both columns.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.get_all, [KSTransaction],
                [kstx_parent_transaction]) as kstx_transaction:

                result = self.__ksc_key_column.get_all(
                    kstx_transaction, b_return_packed_data)

            self.__ksem_event_manager.send_after_event_message(
                self.get_all, [result])
            return result

        except Exception:
            raise


    def values(self, kstx_parent_transaction, b_return_packed_data):
        """
        Retrieve only the values in the secondary column. Please note that
        these are only equivalent to the key values of the mapped column
        since this is a constrained map and not a direct map.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.values, [KSTransaction, bool], [kstx_parent_transaction,
                b_return_packed_data]) as kstx_transaction:

                result = self.__ksc_key_column.values(
                    kstx_transaction, b_return_packed_data)

            self.__ksem_event_manager.send_after_event_message(
                self.values, [result])
            return result

        except Exception:
            raise


    # Use keys, size from parent class


    def print_all(self, kstx_parent_transaction):
        """
        Print all values as they are in both columns.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.print_all, [KSTransaction], [kstx_parent_transaction]) \
                as kstx_transaction:

                result = self.__ksc_key_column.print_all(kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.values, [result])
            return result

        except Exception:
            raise


    def reorganize_keys(self, kstx_parent_transaction):
        """
        Reorganize keys. See KSColumn for more information.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.reorganize_keys, [KSTransaction],
                [kstx_parent_transaction]) as kstx_transaction:

                result = self.__ksc_key_column.reorganize_keys(kstx_transaction)

            self.__ksem_event_manager.send_after_event_message(
                self.reorganize_keys, [result])
            return result

        except Exception:
            raise


    def return_value_type(self, kstx_parent_transaction):
        """
        Return the data type of the stored value in the secondary column. This
        may be somewhat pointless if binary strings of data are stored. The
        pickling procedure for storing values should retrain the original
        object type.

        """
        try:
            with self.__context.complete_transaction(kstx_parent_transaction,
                self.return_value_type, [KSTransaction],
                [kstx_parent_transaction]) as kstx_transaction:

                cursor = self.__ksc_secondary_column.get_cursor(kstx_transaction)
                item = cursor.first()
                item_pickled_data = item[1]
                type_obj = type(self.unpickle_data(item_pickled_data))
                cursor.close()

            kstx_transaction.commit(0, self.return_value_type)
            return type_obj

        except KSCursorError:
            # With an empty database and no record of what type is stored as
            # the value, simply return NoneType because this is consistent
            # with the semantics of the function.
            return type(None)

        except Exception:
            raise


    def generate_id(self):
        """
        Create a unique column ID. Do not send an event because this is a
        trivial function that does not need to be announced.

        """
        try:
            return self.__ksc_key_column.generate_id()

        except Exception:
            KSColumnMappedConstraintError.print_and_raise_error(u'An error '
                + u'occurred while trying to generate a new column ID.',
                KSColumnMappedConstraintError.B_CLASS_EXCEPTION)


    def __check_for_secondary_key(self, obj_key, kstx_transaction):
        """
        This is the function that enforces the constraint that any value
        assigned to this table must exist as a key in the column to which
        it is mapped.

        """
        try:
            b_result = self.__ksc_secondary_column.has_key(
                obj_key, kstx_transaction)

            if (b_result):
                return True
            else:
                KSColumnMappedConstraintError.print_and_raise_error(
                    u'The value must exist as a key in the secondary '
                    + u'column in order to store it as a value in the '
                    + u'primary column.', KSColumnMappedError.B_CLASS_EXCEPTION)

        except Exception:
            raise

    """
    def __type_check_virtual_value(self, obj_value):
        #Retrieve the actual data stored with relation to the value in obj_key.
        return isinstance(obj_value, self.__ksc_secondary_column_type)
    """